آشنایی با ویژگیهای واردات جاوااسکریپت برای ماژولهای JSON. با سینتکس جدید `with { type: 'json' }`، مزایای امنیتی آن و جایگزینی روشهای قدیمی برای کدی پاکتر، ایمنتر و کارآمدتر آشنا شوید.
ویژگیهای واردات جاوااسکریپت (Import Attributes): روشی مدرن و امن برای بارگذاری ماژولهای JSON
سالها بود که توسعهدهندگان جاوااسکریپت با یک وظیفه ظاهراً ساده دستوپنجه نرم میکردند: بارگذاری فایلهای JSON. در حالی که نشانه گذاری شیء جاوااسکریپت (JSON) استاندارد اصلی برای تبادل داده در وب است، ادغام یکپارچه آن در ماژولهای جاوااسکریپت مسیری پر از کدهای تکراری (boilerplate)، راهحلهای موقتی و خطرات امنیتی بالقوه بوده است. از خواندن همزمان فایل در Node.js تا فراخوانیهای پرجزئیات `fetch` در مرورگر، راهحلها بیشتر شبیه وصله بودند تا ویژگیهای بومی. آن دوران اکنون به پایان رسیده است.
به دنیای ویژگیهای واردات (Import Attributes) خوش آمدید؛ راهحلی مدرن، امن و زیبا که توسط TC39، کمیتهای که زبان اکمااسکریپت را مدیریت میکند، استانداردسازی شده است. این ویژگی که با سینتکس ساده اما قدرتمند `with { type: 'json' }` معرفی شده است، در حال متحول کردن نحوه مدیریت داراییهای غیرجاوااسکریپتی ماست، و با رایجترین آنها یعنی JSON شروع میشود. این مقاله یک راهنمای جامع برای توسعهدهندگان جهانی است تا بدانند ویژگیهای واردات چیستند، چه مشکلات مهمی را حل میکنند و چگونه میتوانید از امروز برای نوشتن کدی پاکتر، امنتر و کارآمدتر از آنها استفاده کنید.
دنیای قدیم: نگاهی به گذشته در مدیریت JSON در جاوااسکریپت
برای درک کامل زیبایی ویژگیهای واردات، ابتدا باید چشماندازی را که جایگزین آن میشوند، بشناسیم. بسته به محیط (سمت سرور یا سمت کلاینت)، توسعهدهندگان به تکنیکهای مختلفی تکیه کردهاند که هر کدام مجموعهای از مزایا و معایب خاص خود را دارند.
سمت سرور (Node.js): دوران `require()` و `fs`
در سیستم ماژول CommonJS، که سالها بومی Node.js بود، وارد کردن JSON به طرز فریبندهای ساده بود:
// در یک فایل CommonJS (مثلاً index.js)
const config = require('./config.json');
console.log(config.database.host);
این به زیبایی کار میکرد. Node.js به طور خودکار فایل JSON را به یک شیء جاوااسکریپت تجزیه میکرد. با این حال، با تغییر جهانی به سمت ماژولهای اکمااسکریپت (ESM)، این تابع همزمان `require()` با ماهیت ناهمزمان و top-level-await جاوااسکریپت مدرن ناسازگار شد. معادل مستقیم ESM، یعنی `import`، در ابتدا از ماژولهای JSON پشتیبانی نمیکرد و توسعهدهندگان را مجبور به بازگشت به روشهای قدیمیتر و دستیتر میکرد:
// خواندن دستی فایل در یک فایل ESM (مثلاً index.mjs)
import fs from 'fs';
import path from 'path';
const configPath = path.resolve('config.json');
const configFile = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configFile);
console.log(config.database.host);
این رویکرد چندین نقطه ضعف دارد:
- پرحرفی (Verbosity): برای یک عملیات واحد به چندین خط کد تکراری (boilerplate) نیاز دارد.
- ورودی/خروجی همزمان (Synchronous I/O): `fs.readFileSync` یک عملیات مسدودکننده (blocking) است که میتواند در برنامههای با همزمانی بالا به یک گلوگاه عملکرد تبدیل شود. نسخه ناهمزمان (`fs.readFile`) حتی کد تکراری بیشتری با callbackها یا Promiseها اضافه میکند.
- عدم یکپارچگی: این روش از سیستم ماژول جدا به نظر میرسد و فایل JSON را به عنوان یک فایل متنی عمومی در نظر میگیرد که نیاز به تجزیه دستی دارد.
سمت کلاینت (مرورگرها): کدهای تکراری `fetch` API
در مرورگر، توسعهدهندگان مدتهاست که برای بارگذاری دادههای JSON از یک سرور به `fetch` API تکیه کردهاند. در حالی که این API قدرتمند و انعطافپذیر است، برای کاری که باید یک واردات مستقیم باشد، پرحرف است.
// الگوی کلاسیک fetch
let config;
fetch('/config.json')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json(); // بدنه JSON را تجزیه میکند
})
.then(data => {
config = data;
console.log(config.api.key);
})
.catch(error => console.error('Error fetching config:', error));
این الگو، هرچند مؤثر، از مشکلات زیر رنج میبرد:
- کد تکراری (Boilerplate): هر بارگذاری JSON به زنجیره مشابهی از Promiseها، بررسی پاسخ و مدیریت خطا نیاز دارد.
- سربار ناهمزمانی (Asynchronicity Overhead): مدیریت ماهیت ناهمزمان `fetch` میتواند منطق برنامه را پیچیده کند و اغلب برای مدیریت فاز بارگذاری به مدیریت وضعیت نیاز دارد.
- عدم تحلیل ایستا (No Static Analysis): از آنجایی که این یک فراخوانی در زمان اجرا است، ابزارهای ساخت (build tools) نمیتوانند به راحتی این وابستگی را تحلیل کنند و به طور بالقوه بهینهسازیها را از دست میدهند.
یک گام به جلو: `import()` پویا با ادعاها (پیشرو)
با تشخیص این چالشها، کمیته TC39 ابتدا ادعاهای واردات (Import Assertions) را پیشنهاد داد. این یک گام مهم به سوی یک راهحل بود که به توسعهدهندگان اجازه میداد فرادادهای درباره یک واردات ارائه دهند.
// پروپوزال اصلی Import Assertions
const configModule = await import('./config.json', { assert: { type: 'json' } });
const config = configModule.default;
این یک پیشرفت بزرگ بود. این ویژگی بارگذاری JSON را با سیستم ESM یکپارچه کرد. عبارت `assert` به موتور جاوااسکریپت میگفت که تأیید کند منبع بارگذاریشده واقعاً یک فایل JSON است. با این حال، در طی فرآیند استانداردسازی، یک تمایز معنایی مهم پدیدار شد که منجر به تکامل آن به ویژگیهای واردات شد.
ورود ویژگیهای واردات: رویکردی اعلانی و امن
پس از بحثهای گسترده و بازخورد از پیادهسازان موتورها، ادعاهای واردات به ویژگیهای واردات (Import Attributes) اصلاح شدند. سینتکس کمی متفاوت است، اما تغییر معنایی عمیق است. این روش جدید و استاندارد برای وارد کردن ماژولهای JSON است:
واردات ایستا (Static Import):
import config from './config.json' with { type: 'json' };
واردات پویا (Dynamic Import):
const configModule = await import('./config.json', { with: { type: 'json' } });
const config = configModule.default;
کلمه کلیدی `with`: فراتر از یک تغییر نام
تغییر از `assert` به `with` صرفاً ظاهری نیست. این تغییر نشاندهنده یک جابجایی اساسی در هدف است:
- `assert { type: 'json' }`: این سینتکس به معنای یک تأیید پس از بارگذاری بود. موتور ماژول را دریافت میکرد و سپس بررسی میکرد که آیا با ادعا مطابقت دارد یا خیر. اگر مطابقت نداشت، خطا پرتاب میکرد. این عمدتاً یک بررسی امنیتی بود.
- `with { type: 'json' }`: این سینتکس به معنای یک دستورالعمل پیش از بارگذاری است. این سینتکس اطلاعاتی را به محیط میزبان (مرورگر یا Node.js) در مورد چگونگی بارگذاری و تجزیه ماژول از همان ابتدا ارائه میدهد. این فقط یک بررسی نیست؛ یک دستورالعمل است.
این تمایز حیاتی است. کلمه کلیدی `with` به موتور جاوااسکریپت میگوید: "من قصد دارم منبعی را وارد کنم و ویژگیهایی را برای راهنمایی فرآیند بارگذاری در اختیار شما قرار میدهم. از این اطلاعات برای انتخاب بارگذارنده صحیح و اعمال سیاستهای امنیتی مناسب از همان ابتدا استفاده کنید." این امر امکان بهینهسازی بهتر و یک قرارداد واضحتر بین توسعهدهنده و موتور را فراهم میکند.
چرا این یک تغییر بزرگ است؟ ضرورت امنیتی
مهمترین مزیت ویژگیهای واردات، امنیت است. آنها برای جلوگیری از دستهای از حملات معروف به سردرگمی نوع MIME (MIME-type confusion) طراحی شدهاند که میتواند منجر به اجرای کد از راه دور (RCE) شود.
تهدید RCE با وارداتهای مبهم
سناریویی را بدون ویژگیهای واردات تصور کنید که در آن از یک واردات پویا برای بارگذاری یک فایل پیکربندی از یک سرور استفاده میشود:
// واردات بالقوه ناامن
const { settings } = await import('https://api.example.com/user-settings.json');
چه اتفاقی میافتد اگر سرور در `api.example.com` به خطر بیفتد؟ یک عامل مخرب میتواند نقطه پایانی `user-settings.json` را تغییر دهد تا به جای یک فایل JSON، یک فایل جاوااسکریپت را ارائه دهد، در حالی که هنوز پسوند `.json` را حفظ میکند. سرور کد اجرایی را با هدر `Content-Type` از نوع `text/javascript` بازمیگرداند.
بدون مکانیزمی برای بررسی نوع، موتور جاوااسکریپت ممکن است کد جاوااسکریپت را ببیند و آن را اجرا کند و به مهاجم کنترل جلسه کاربر را بدهد. این یک آسیبپذیری امنیتی شدید است.
چگونه ویژگیهای واردات این خطر را کاهش میدهند
ویژگیهای واردات این مشکل را به زیبایی حل میکنند. وقتی واردات را با این ویژگی مینویسید، یک قرارداد سخت با موتور ایجاد میکنید:
// واردات امن
const { settings } = await import('https://api.example.com/user-settings.json' with { type: 'json' });
اینجا چه اتفاقی میافتد:
- مرورگر `user-settings.json` را درخواست میکند.
- سرور، که اکنون به خطر افتاده است، با کد جاوااسکریپت و هدر `Content-Type: text/javascript` پاسخ میدهد.
- بارگذارنده ماژول مرورگر میبیند که نوع MIME پاسخ (`text/javascript`) با نوع مورد انتظار از ویژگی واردات (`json`) مطابقت ندارد.
- به جای تجزیه یا اجرای فایل، موتور بلافاصله یک `TypeError` پرتاب میکند، عملیات را متوقف کرده و از اجرای هرگونه کد مخرب جلوگیری میکند.
این افزودنی ساده، یک آسیبپذیری بالقوه RCE را به یک خطای زمان اجرای امن و قابل پیشبینی تبدیل میکند. این تضمین میکند که دادهها، داده باقی میمانند و هرگز به اشتباه به عنوان کد اجرایی تفسیر نمیشوند.
موارد استفاده عملی و مثالهای کد
ویژگیهای واردات برای JSON فقط یک ویژگی امنیتی نظری نیستند. آنها بهبودهای ارگونومیکی را برای وظایف توسعه روزمره در حوزههای مختلف به ارمغان میآورند.
۱. بارگذاری پیکربندی برنامه
این کلاسیکترین مورد استفاده است. به جای ورودی/خروجی دستی فایل، اکنون میتوانید پیکربندی خود را به طور مستقیم و ایستا وارد کنید.
فایل: `config.json`
{
"database": {
"host": "db.production.example.com",
"port": 5432,
"user": "api_user"
},
"featureFlags": {
"newDashboard": true,
"enableLogging": false
}
}
فایل: `database.mjs`
import config from './config.json' with { type: 'json' };
export function getDbHost() {
return config.database.host;
}
console.log(`Connecting to database at: ${getDbHost()}`);
این کد پاک، اعلانی و برای انسانها و ابزارهای ساخت به راحتی قابل درک است.
۲. دادههای بینالمللیسازی (i18n)
مدیریت ترجمهها یکی دیگر از کاربردهای عالی است. شما میتوانید رشتههای زبان را در فایلهای JSON جداگانه ذخیره کرده و در صورت نیاز آنها را وارد کنید.
فایل: `locales/en-US.json`
{
"welcomeMessage": "Hello, welcome to our application!",
"logoutButton": "Log Out"
}
فایل: `locales/fa-IR.json`
{
"welcomeMessage": "سلام، به برنامه ما خوش آمدید!",
"logoutButton": "خروج از سیستم"
}
فایل: `i18n.mjs`
// وارد کردن استاتیک زبان پیشفرض
import defaultStrings from './locales/en-US.json' with { type: 'json' };
// وارد کردن پویا زبانهای دیگر بر اساس ترجیح کاربر
async function getTranslations(locale) {
if (locale === 'fa-IR') {
const module = await import('./locales/fa-IR.json', { with: { type: 'json' } });
return module.default;
}
return defaultStrings;
}
const userLocale = 'fa-IR';
const strings = await getTranslations(userLocale);
console.log(strings.welcomeMessage); // پیام فارسی را خروجی میدهد
۳. بارگذاری دادههای ایستا برای برنامههای وب
تصور کنید یک منوی کشویی را با لیستی از کشورها پر میکنید یا یک کاتالوگ محصول را نمایش میدهید. این دادههای ایستا را میتوان در یک فایل JSON مدیریت کرد و مستقیماً به کامپوننت خود وارد کرد.
فایل: `data/countries.json`
[
{ "code": "US", "name": "United States" },
{ "code": "DE", "name": "Germany" },
{ "code": "JP", "name": "Japan" }
]
فایل: `CountrySelector.js` (کامپوننت فرضی)
import countries from '../data/countries.json' with { type: 'json' };
export class CountrySelector {
constructor(elementId) {
this.element = document.getElementById(elementId);
this.render();
}
render() {
const options = countries.map(country =>
``
).join('');
this.element.innerHTML = options;
}
}
// استفاده
new CountrySelector('country-dropdown');
چگونه در پشت صحنه کار میکند: نقش محیط میزبان
رفتار ویژگیهای واردات توسط محیط میزبان تعریف میشود. این بدان معناست که تفاوتهای جزئی در پیادهسازی بین مرورگرها و رانتایمهای سمت سرور مانند Node.js وجود دارد، اگرچه نتیجه سازگار است.
در مرورگر
در زمینه مرورگر، فرآیند به شدت با استانداردهای وب مانند HTTP و انواع MIME گره خورده است.
- وقتی مرورگر با `import data from './data.json' with { type: 'json' }` مواجه میشود، یک درخواست HTTP GET برای `./data.json` آغاز میکند.
- سرور درخواست را دریافت میکند و باید با محتوای JSON پاسخ دهد. نکته مهم این است که پاسخ HTTP سرور باید شامل هدر `Content-Type: application/json` باشد.
- مرورگر پاسخ را دریافت کرده و هدر `Content-Type` را بررسی میکند.
- مقدار هدر را با `type` مشخص شده در ویژگی واردات مقایسه میکند.
- اگر مطابقت داشته باشند، مرورگر بدنه پاسخ را به عنوان JSON تجزیه کرده و شیء ماژول را ایجاد میکند.
- اگر مطابقت نداشته باشند (مثلاً سرور `text/html` یا `text/javascript` ارسال کرده باشد)، مرورگر بارگذاری ماژول را با یک `TypeError` رد میکند.
در Node.js و سایر رانتایمها
برای عملیات فایل سیستم محلی، Node.js و Deno از انواع MIME استفاده نمیکنند. در عوض، آنها به ترکیبی از پسوند فایل و ویژگی واردات برای تعیین نحوه مدیریت فایل تکیه میکنند.
- وقتی بارگذارنده ESM در Node.js عبارت `import config from './config.json' with { type: 'json' }` را میبیند، ابتدا مسیر فایل را شناسایی میکند.
- از ویژگی `with { type: 'json' }` به عنوان یک سیگنال قوی برای انتخاب بارگذارنده ماژول JSON داخلی خود استفاده میکند.
- بارگذارنده JSON محتویات فایل را از دیسک میخواند.
- محتویات را به عنوان JSON تجزیه میکند. اگر فایل حاوی JSON نامعتبر باشد، یک خطای نحوی (syntax error) پرتاب میشود.
- یک شیء ماژول ایجاد و بازگردانده میشود، که معمولاً دادههای تجزیه شده را به عنوان خروجی `default` دارد.
این دستورالعمل صریح از ویژگی، از ابهام جلوگیری میکند. Node.js به طور قطعی میداند که نباید تلاش کند فایل را به عنوان جاوااسکریپت اجرا کند، صرف نظر از محتوای آن.
پشتیبانی مرورگر و رانتایم: آیا برای تولید آماده است؟
پذیرش یک ویژگی جدید زبان نیازمند بررسی دقیق پشتیبانی آن در محیطهای هدف است. خوشبختانه، ویژگیهای واردات برای JSON شاهد پذیرش سریع و گستردهای در اکوسیستم جاوااسکریپت بودهاند. از اواخر سال ۲۰۲۳، پشتیبانی در محیطهای مدرن عالی است.
- Google Chrome / موتورهای Chromium (Edge, Opera): از نسخه 117 پشتیبانی میشود.
- Mozilla Firefox: از نسخه 121 پشتیبانی میشود.
- Safari (WebKit): از نسخه 17.2 پشتیبانی میشود.
- Node.js: از نسخه 21.0 به طور کامل پشتیبانی میشود. در نسخههای قبلی (مانند v18.19.0+، v20.10.0+)، این ویژگی پشت فلگ `--experimental-import-attributes` در دسترس بود.
- Deno: به عنوان یک رانتایم پیشرو، Deno از این ویژگی (که از assertions تکامل یافته) از نسخه 1.34 پشتیبانی میکند.
- Bun: از نسخه 1.0 پشتیبانی میشود.
برای پروژههایی که نیاز به پشتیبانی از مرورگرها یا نسخههای قدیمیتر Node.js دارند، ابزارهای ساخت مدرن و باندلرها مانند Vite، Webpack (با لودرهای مناسب) و Babel (با یک پلاگین تبدیل) میتوانند سینتکس جدید را به فرمت سازگار تبدیل کنند و به شما این امکان را میدهند که امروز کد مدرن بنویسید.
فراتر از JSON: آینده ویژگیهای واردات
در حالی که JSON اولین و برجستهترین مورد استفاده است، سینتکس `with` به گونهای طراحی شده که قابل توسعه باشد. این یک مکانیزم عمومی برای پیوست کردن فراداده به واردات ماژول فراهم میکند و راه را برای ادغام انواع دیگر منابع غیرجاوااسکریپتی در سیستم ماژول ES هموار میسازد.
اسکریپتهای ماژول CSS
ویژگی بزرگ بعدی در افق، اسکریپتهای ماژول CSS است. این پروپوزال به توسعهدهندگان اجازه میدهد تا شیوهنامههای CSS را مستقیماً به عنوان ماژول وارد کنند:
import sheet from './styles.css' with { type: 'css' };
document.adoptedStyleSheets = [sheet];
وقتی یک فایل CSS به این روش وارد میشود، به یک شیء `CSSStyleSheet` تجزیه میشود که میتوان آن را به صورت برنامهریزی شده به یک سند یا Shadow DOM اعمال کرد. این یک جهش بزرگ برای کامپوننتهای وب و استایلدهی پویا است و نیاز به تزریق تگهای `